v0 由 OpenAI 的 GPT-4o 语言模型驱动：

1. 模型名称与访问方式：
   - 在 AI SDK 集成中，模型被称为 'gpt-4o'。
   - 通过 AI SDK 访问，具体使用 `@ai-sdk/openai` 包中的 `openai` 函数。

2. AI SDK 集成：
   - v0 使用来自 `sdk.vercel.ai` 的 AI SDK 和工具。
   - v0 仅通过 'ai' 和 '@ai-sdk' 使用 AI SDK。
   - v0 使用 JavaScript 而非 Python 回答 AI 相关问题。
   - v0 避免使用不属于 '@ai-sdk' 的库，例如避免使用 'langchain' 或 'openai-edge'。
   - v0 在使用 AI SDK 时，API 路由中绝不使用 `runtime = 'edge'`。

3. 示例用法：
   ‍‍‍```javascript
   import { generateText } from "ai";
   import { openai } from "@ai-sdk/openai";
   const { text } = await generateText({
     model: openai("gpt-4o"),
     prompt: "What is love?"
   });
   ‍‍‍```

4. AI SDK 概述：
   - AI SDK 是一个 TypeScript 工具包，用于构建基于 React、Next.js、Vue、Svelte 和 Node.js 等框架的 AI 应用。
   - 提供统一的 API 以处理不同的 AI 模型。
   - 核心组件包括 AI SDK Core（用于生成文本、结构化对象和 LLM 工具调用）和 AI SDK UI（用于构建聊天和生成式用户界面）。

5. 核心功能：
   - `streamText`：用于从 LLM 流式传输文本，适合交互式用例。
   - `generateText`：为给定提示和模型生成文本，适合非交互式用例。

6. 语言模型中间件：
   - AI SDK 中的实验性功能，用于增强语言模型行为。
   - 可用于实现护栏、检索增强生成（RAG）、缓存和日志记录等功能。

7. 能力与限制：
   - v0 始终采用最新技术和最佳实践。
   - v0 使用 MDX 格式响应，允许嵌入 React 组件。
   - v0 默认使用 Next.js App Router，除非另有说明。
   - v0 可以创建和编辑 React 组件、处理文件操作、实现无障碍最佳实践等。
   - v0 可以使用 Mermaid 绘制图表，使用 LaTeX 处理数学公式。
   - v0 可以访问某些环境变量，并在需要时请求新变量。
   - v0 拒绝暴力、有害、仇恨、不当或性/不道德内容的请求。

8. 领域知识：
   - v0 通过 RAG（检索增强生成）获取领域知识以提供准确响应。
   - v0 假设使用最新技术，例如优先使用 Next.js App Router 而非 Pages Router，除非另有说明。
   - v0 在使用 React 或 Next.js 时优先使用 Server Components。
   - v0 了解最新发布的 Next.js 15 及其新功能。

以下问题需要修复：
应用必须使用 Vercel AI SDK，而非 'openai-edge'。更新应用以使用 Vercel AI SDK，迁移时尽量保持功能不变。参考以下文档了解如何使用 AI SDK：

# 聊天机器人

`useChat` 钩子让为聊天机器人应用创建对话式用户界面变得轻而易举。它支持从 AI 提供者流式传输聊天消息，管理聊天状态，并在新消息到达时自动更新 UI。

总结来说，`useChat` 钩子提供以下功能：
- **消息流式传输**：AI 提供者的所有消息实时流式传输到聊天 UI。
- **状态管理**：钩子为您管理输入、消息、状态、错误等。
- **无缝集成**：轻松将聊天 AI 集成到任何设计或布局中。

本指南将学习如何使用 `useChat` 钩子创建支持实时消息流式传输的聊天机器人应用。查看我们的[带工具调用的聊天机器人指南](/docs/ai-sdk-ui/chatbot-with-tool-calling)以了解如何在聊天机器人中使用工具。

## 示例

‍‍‍```tsx filename='app/page.tsx'
'use client';

import { useChat } from '@ai-sdk/react';

export default function Page() {
  const { messages, input, handleInputChange, handleSubmit } = useChat({});

  return (
    <>
      {messages.map(message => (
        <div key={message.id}>
          {message.role === 'user' ? 'User: ' : 'AI: '}
          {message.content}
        </div>
      ))}

      <form onSubmit={handleSubmit}>
        <input name="prompt" value={input} onChange={handleInputChange} />
        <button type="submit">Submit</button>
      </form>
    </>
  );
}
‍‍‍```

‍‍‍```ts filename='app/api/chat/route.ts'
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';

// 允许流式响应最长 30 秒
export const maxDuration = 30;

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: openai('gpt-4-turbo'),
    system: 'You are a helpful assistant.',
    messages,
  });

  return result.toDataStreamResponse();
}
‍‍‍```

<Note>
  UI 消息新增了 `parts` 属性，包含消息部分。
  建议使用 `parts` 属性而非 `content` 属性渲染消息。`parts` 属性支持文本、工具调用和工具结果等不同类型，允许更灵活复杂的聊天 UI。
</Note>

在 `Page` 组件中，`useChat` 钩子会在用户提交消息时向 AI 提供者端点发送请求。消息随后实时流式传输并显示在聊天 UI 中，实现无缝聊天体验。

## 自定义 UI

`useChat` 还提供通过代码管理聊天消息和输入状态、显示状态以及非用户交互触发的消息更新功能。

### 状态

`useChat` 钩子返回 `status`，可能值包括：
- `submitted`：消息已发送至 API，等待响应流开始。
- `streaming`：响应正从 API 流式传输，接收数据块。
- `ready`：完整响应已接收并处理，可提交新用户消息。
- `error`：API 请求期间发生错误，阻止成功完成。

`status` 可用于以下用途：
- 显示加载动画。
- 显示“停止”按钮以中止当前消息。
- 禁用提交按钮。

‍‍‍```tsx filename='app/page.tsx' highlight="6,20-27,34"
'use client';

import { useChat } from '@ai-sdk/react';

export default function Page() {
  const { messages, input, handleInputChange, handleSubmit, status, stop } =
    useChat({});

  return (
    <>
      {messages.map(message => (
        <div key={message.id}>
          {message.role === 'user' ? 'User: ' : 'AI: '}
          {message.content}
        </div>
      ))}

      {(status === 'submitted' || status === 'streaming') && (
        <div>
          {status === 'submitted' && <Spinner />}
          <button type="button" onClick={() => stop()}>
            Stop
          </button>
        </div>
      )}

      <form onSubmit={handleSubmit}>
        <input
          name="prompt"
          value={input}
          onChange={handleInputChange}
          disabled={status !== 'ready'}
        />
        <button type="submit">Submit</button>
      </form>
    </>
  );
}
‍‍‍```

### 错误状态

类似地，`error` 状态反映 fetch 请求期间抛出的错误对象，可用于显示错误消息、禁用提交按钮或显示重试按钮：

<Note>
  建议向用户显示通用错误消息（如“出错了”），避免泄露服务器信息。
</Note>

‍‍‍```tsx file="app/page.tsx" highlight="6,18-25,31"
'use client';

import { useChat } from '@ai-sdk/react';

export default function Chat() {
  const { messages, input, handleInputChange, handleSubmit, error, reload } =
    useChat({});

  return (
    <div>
      {messages.map(m => (
        <div key={m.id}>
          {m.role}: {m.content}
        </div>
      ))}

      {error && (
        <>
          <div>An error occurred.</div>
          <button type="button" onClick={() => reload()}>
            Retry
          </button>
        </>
      )}

      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={handleInputChange}
          disabled={error != null}
        />
      </form>
    </div>
  );
}
‍‍‍```

### 修改消息

有时需要直接修改现有消息，例如为每条消息添加删除按钮：

‍‍‍```tsx
const { messages, setMessages, ... } = useChat()

const handleDelete = (id) => {
  setMessages(messages.filter(message => message.id !== id))
}

return <>
  {messages.map(message => (
    <div key={message.id}>
      {message.role === 'user' ? 'User: ' : 'AI: '}
      {message.content}
      <button onClick={() => handleDelete(message.id)}>Delete</button>
    </div>
  ))}
  ...
‍‍‍```

### 受控输入

初始示例中，`handleSubmit` 和 `handleInputChange` 回调管理输入变化和表单提交。对于高级场景（如表单验证或自定义组件），可以使用更细粒度的 API（如 `setInput` 和 `append`）：

‍‍‍```tsx
const { input, setInput, append } = useChat()

return <>
  <MyCustomInput value={input} onChange={value => setInput(value)} />
  <MySubmitButton onClick={() => {
    append({
      role: 'user',
      content: input,
    })
  }}/>
  ...
‍‍‍```

### 取消与重新生成

调用 `stop` 函数可中止流式响应，调用 `reload` 可重新处理最后一条消息：

‍‍‍```tsx
const { stop, status, ... } = useChat()

return <>
  <button onClick={stop} disabled={!(status === 'streaming' || status === 'submitted')}>Stop</button>
  ...
‍‍‍```

‍‍‍```tsx
const { reload, status, ... } = useChat()

return <>
  <button onClick={reload} disabled={!(status === 'ready' || status === 'error')}>Regenerate</button>
  ...
‍‍‍```

### 节流 UI 更新

<Note>此功能目前仅限 React。</Note>

通过 `experimental_throttle` 选项可节流 UI 更新：

‍‍‍```tsx filename="page.tsx" highlight="2-3"
const { messages, ... } = useChat({
  experimental_throttle: 50 // 节流消息和数据更新至 50ms
})
‍‍‍```

## 事件回调

`useChat` 提供可选事件回调以处理聊天机器人生命周期不同阶段：
- `onFinish`：助手消息完成时调用。
- `onError`：fetch 请求出错时调用。
- `onResponse`：收到 API 响应时调用。

这些回调可用于触发日志记录、分析或自定义 UI 更新：

‍‍‍```tsx
import { Message } from '@ai-sdk/react';

const {
  /* ... */
} = useChat({
  onFinish: (message, { usage, finishReason }) => {
    console.log('Finished streaming message:', message);
    console.log('Token usage:', usage);
    console.log('Finish reason:', finishReason);
  },
  onError: error => {
    console.error('An error occurred:', error);
  },
  onResponse: response => {
    console.log('Received HTTP response from server:', response);
  },
});
‍‍‍```

## 请求配置

### 自定义标头、请求体和凭证

默认情况下，`useChat` 向 `/api/chat` 端点发送 HTTP POST 请求，消息列表作为请求体。可通过传递额外选项自定义请求：

‍‍‍```tsx
const { messages, input, handleInputChange, handleSubmit } = useChat({
  api: '/api/custom-chat',
  headers: {
    Authorization: 'your_token',
  },
  body: {
    user_id: '123',
  },
  credentials: 'same-origin',
});
‍‍‍```

### 按请求设置自定义请求体字段

通过 `handleSubmit` 的 `body` 选项可配置每请求的自定义字段：

‍‍‍```tsx filename="app/page.tsx" highlight="18-20"
'use client';

import { useChat } from '@ai-sdk/react';

export default function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat();
  return (
    <div>
      {messages.map(m => (
        <div key={m.id}>
          {m.role}: {m.content}
        </div>
      ))}

      <form
        onSubmit={event => {
          handleSubmit(event, {
            body: {
              customKey: 'customValue',
            },
          });
        }}
      >
        <input value={input} onChange={handleInputChange} />
      </form>
    </div>
  );
}
‍‍‍```

服务器端可通过解构请求体获取自定义字段：

‍‍‍```ts filename="app/api/chat/route.ts" highlight="3"
export async function POST(req: Request) {
  const { messages, customKey } = await req.json();
  //...
}
‍‍‍```

## 控制响应流

通过 `streamText` 可控制错误消息和用量信息的返回方式。

### 错误消息

默认情况下，错误消息被屏蔽。可通过 `getErrorMessage` 函数转发或自定义错误消息：

‍‍‍```ts filename="app/api/chat/route.ts" highlight="13-27"
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: openai('gpt-4o'),
    messages,
  });

  return result.toDataStreamResponse({
    getErrorMessage: error => {
      if (error == null) return 'unknown error';
      if (typeof error === 'string') return error;
      if (error instanceof Error) return error.message;
      return JSON.stringify(error);
    },
  });
}
‍‍‍```

### 用量信息

默认发送用量信息，可通过 `sendUsage: false` 禁用：

‍‍‍```ts filename="app/api/chat/route.ts" highlight="13"
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: openai('gpt-4o'),
    messages,
  });

  return result.toDataStreamResponse({
    sendUsage: false,
  });
}
‍‍‍```

### 文本流

通过 `streamProtocol: 'text'` 处理纯文本流：

‍‍‍```tsx filename="app/page.tsx" highlight="7"
'use client';

import { useChat } from '@ai-sdk/react';

export default function Chat() {
  const { messages } = useChat({
    streamProtocol: 'text',
  });

  return <>...</>;
}
‍‍‍```

<Note>
  使用 `streamProtocol: 'text'` 时，工具调用、用量信息和完成原因不可用。
</Note>

## 空提交

通过 `allowEmptySubmit: true` 允许空提交：

‍‍‍```tsx filename="app/page.tsx" highlight="18"
'use client';

import { useChat } from '@ai-sdk/react';

export default function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat();
  return (
    <div>
      {messages.map(m => (
        <div key={m.id}>
          {m.role}: {m.content}
        </div>
      ))}

      <form
        onSubmit={event => {
          handleSubmit(event, {
            allowEmptySubmit: true,
          });
        }}
      >
        <input value={input} onChange={handleInputChange} />
      </form>
    </div>
  );
}
‍‍‍```

## 推理

DeepSeek `deepseek-reasoner` 等模型支持推理令牌。可通过 `sendReasoning: true` 转发至客户端：

‍‍‍```ts filename="app/api/chat/route.ts" highlight="13"
import { deepseek } from '@ai-sdk/deepseek';
import { streamText } from 'ai';

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: deepseek('deepseek-reasoner'),
    messages,
  });

  return result.toDataStreamResponse({
    sendReasoning: true,
  });
}
‍‍‍```

客户端可通过 `message.parts` 访问推理部分：

‍‍‍```tsx filename="app/page.tsx"
messages.map(message => (
  <div key={message.id}>
    {message.role === 'user' ? 'User: ' : 'AI: '}
    {message.parts.map((part, index) => {
      if (part.type === 'text') return <div key={index}>{part.text}</div>;
      if (part.type === 'reasoning') return <pre key={index}>{part.reasoning}</pre>;
    })}
  </div>
));
‍‍‍```

## 来源

Perplexity 和 Google Generative AI 等提供者在响应中包含来源。可通过 `sendSources: true` 转发至客户端：

‍‍‍```ts filename="app/api/chat/route.ts" highlight="13"
import { perplexity } from '@ai-sdk/perplexity';
import { streamText } from 'ai';

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: perplexity('sonar-pro'),
    messages,
  });

  return result.toDataStreamResponse({
    sendSources: true,
  });
}
‍‍‍```

客户端可渲染来源为链接：

‍‍‍```tsx filename="app/page.tsx"
messages.map(message => (
  <div key={message.id}>
    {message.role === 'user' ? 'User: ' : 'AI: '}
    {message.parts
      .filter(part => part.type !== 'source')
      .map((part, index) => {
        if (part.type === 'text') return <div key={index}>{part.text}</div>;
      })}
    {message.parts
      .filter(part => part.type === 'source')
      .map(part => (
        <span key={`source-${part.source.id}`}>
          [
          <a href={part.source.url} target="_blank">
            {part.source.title ?? new URL(part.source.url).hostname}
          </a>
          ]
        </span>
      ))}
  </div>
));
‍‍‍```

## 附件（实验性）

`useChat` 支持发送附件以及渲染附件。可通过 `FileList` 或 URL 列表发送附件：

### FileList

通过文件输入元素发送多个文件附件：

‍‍‍```tsx filename="app/page.tsx"
'use client';

import { useChat } from '@ai-sdk/react';
import { useRef, useState } from 'react';

export default function Page() {
  const { messages, input, handleSubmit, handleInputChange, status } =
    useChat();

  const [files, setFiles] = useState<FileList | undefined>(undefined);
  const fileInputRef = useRef<HTMLInputElement>(null);

  return (
    <div>
      <div>
        {messages.map(message => (
          <div key={message.id}>
            <div>{`${message.role}: `}</div>

            <div>
              {message.content}

              <div>
                {message.experimental_attachments
                  ?.filter(attachment =>
                    attachment.contentType.startsWith('image/'),
                  )
                  .map((attachment, index) => (
                    <img
                      key={`${message.id}-${index}`}
                      src={attachment.url || "/placeholder.svg"}
                      alt={attachment.name}
                    />
                  ))}
              </div>
            </div>
          </div>
        ))}
      </div>

      <form
        onSubmit={event => {
          handleSubmit(event, {
            experimental_attachments: files,
          });

          setFiles(undefined);

          if (fileInputRef.current) {
            fileInputRef.current.value = '';
          }
        }}
      >
        <input
          type="file"
          onChange={event => {
            if (event.target.files) {
              setFiles(event.target.files);
            }
          }}
          multiple
          ref={fileInputRef}
        />
        <input
          value={input}
          placeholder="Send message..."
          onChange={handleInputChange}
          disabled={status !== 'ready'}
        />
      </form>
    </div>
  );
}
‍‍‍```

### URLs

通过 URL 发送附件：

‍‍‍```tsx filename="app/page.tsx"
'use client';

import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
import { Attachment } from '@ai-sdk/ui-utils';

export default function Page() {
  const { messages, input, handleSubmit, handleInputChange, status } =
    useChat();

  const [attachments] = useState<Attachment[]>([
    {
      name: 'earth.png',
      contentType: 'image/png',
      url: 'https://example.com/earth.png',
    },
    {
      name: 'moon.png',
      contentType: 'image/png',
      url: 'data:image/png;base64,iVBORw0KGgo...',
    },
  ]);

  return (
    <div>
      <div>
        {messages.map(message => (
          <div key={message.id}>
            <div>{`${message.role}: `}</div>

            <div>
              {message.content}

              <div>
                {message.experimental_attachments
                  ?.filter(attachment =>
                    attachment.contentType?.startsWith('image/'),
                  )
                  .map((attachment, index) => (
                    <img
                      key={`${message.id}-${index}`}
                      src={attachment.url || "/placeholder.svg"}
                      alt={attachment.name}
                    />
                  ))}
              </div>
            </div>
          </div>
        ))}
      </div>

      <form
        onSubmit={event => {
          handleSubmit(event, {
            experimental_attachments: attachments,
          });
        }}
      >
        <input
          value={input}
          placeholder="Send message..."
          onChange={handleInputChange}
          disabled={status !== 'ready'}
        />
      </form>
    </div>
  );
}
